import java.nio.ByteBuffer

class Bencoder {

    private static encode(BigInteger val) {
        return "i${val}e"
    }

    private static encode(String val) {
        return String.format('%d:%s', val.length(), val)
    }

    private static encode(List val) {
        def builder = new StringBuilder('l')
        val.each {
            builder += encode(it)
        }
        builder += 'e'
        return builder.toString()
    }

    private static encode(Map val) {
        def builder = new StringBuilder('d')
        val.each { key, value ->
            builder += (encode(String.valueOf(key)) + encode(value))
        }
        builder += 'e'
        return builder.toString()
    }

    static decodeNext(ByteBuffer byteBuffer) {
        def char indicator = byteBuffer.asReadOnlyBuffer().get() as char
        switch(indicator) {
            case ~/[0-9]/:
                return decodeString(byteBuffer)
            case 'i':
                return decodeInt(byteBuffer)
            case 'l':
                return decodeList(byteBuffer)
            case 'd':
                return decodeMap(byteBuffer)
            default:
                throw new Exception("Out-of-sync: '$indicator'")
        }
    }

    private static decodeInt(ByteBuffer byteBuffer) {
        assert byteBuffer.get() as char == 'i'
        return decodeTerminatedInt(byteBuffer, 'e' as char)
    }

    private static decodeString(ByteBuffer byteBuffer) {
        def int length = decodeTerminatedInt(byteBuffer, ':' as char)
        def byte[] data = new byte[length]
        byteBuffer.get(data)
        return new String(data)
    }

    private static decodeList(ByteBuffer byteBuffer) {
        assert byteBuffer.get() as char == 'l'
        def values = []
        while(byteBuffer.asReadOnlyBuffer().get() != 'e') {
            values += decodeString(byteBuffer)
        }
        assert byteBuffer.get() as char == 'e'
        return values
    }

    private static decodeMap(ByteBuffer byteBuffer) {
        assert byteBuffer.get() as char == 'd'
        def map = [:]
        while(byteBuffer.asReadOnlyBuffer().get() != 'e') {
            map.put(decodeString(byteBuffer), decodeNext(byteBuffer))
        }
        assert byteBuffer.get() as char == 'e'
        return map
    }

    private static decodeTerminatedInt(ByteBuffer byteBuffer, char terminator) {
        def s = new StringBuilder()
        def byte b
        while((b = byteBuffer.get()) != terminator) {
            s += b as char
        }
        // There is no theoretical limit on integer sizes in Bencoding
        return new BigInteger(s.toString())
    }
}
